<template>
  <div class="ex-upload-wrap">
    <el-upload v-bind="customAttrs" v-on="customListeners" :on-change="onUploadChange" :on-success="onUploadSuccess"
      :on-remove="onUploadRemove" :on-exceed="onUploadExceed" :before-upload="onBeforeUpload" :on-preview="onFilePreview"
      :ref="refId">
      <slot>
        <i v-if="customAttrs.listType === 'picture-card' || customAttrs['list-type'] === 'picture-card'"
          class="el-icon-plus"></i>
        <el-button v-else icon="el-icon-upload2">
          上传文件
        </el-button>
      </slot>
      <div v-if="$slots.tip" slot="tip" class="el-upload__tip">
        <slot name="tip"></slot>
      </div>
    </el-upload>
    <!-- 如果是文件夹上传 -->
    <div v-if="directory && showDirectory && value.length" class="directory-list-main">
      <div class="progress-item" v-for="(item) in value" :key="item.dirname" status="success">
        <span class="name-box">
          <span class="name">{{ item.dirname }}({{ item.current }}/{{ item.total }})</span>
          <i v-if="!customAttrs.disabled" class="el-icon-close" @click="onUploadRemove(item, value)"></i>
        </span>
        <el-progress :percentage="(item.current / item.total) * 100 || 0" :format="v => `${parseInt(v)}%`"></el-progress>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios'

export default {
  inheritAttrs: false,
  props: {
    // v-model
    value: {
      type: Array,
      default: () => []
    },
    // 上传文件夹
    directory: {
      type: Boolean,
      default: false
    },
    // 同 el-upload 的 action
    action: {
      type: String,
      default: ''
    },
    // 自定义文件预览路径
    downloadUrl: {
      type: String,
      default: ''
    },
    // 文件夹上传时，不显示具体的文件列表，显示成文件夹的形式，同时 showFileList 自动变成 false
    showDirectory: {
      type: Boolean,
      default: true
    },
    // 同 el-upload 的 on-change
    onChange: {
      type: Function,
      default: undefined
    },
    // 同 el-upload 的 on-success
    onSuccess: {
      type: Function,
      default: undefined
    },
    // 同 el-upload 的 on-remove
    onRemove: {
      type: Function,
      default: undefined
    },
    // 同 el-upload 的 on-exceed
    onExceed: {
      type: Function,
      default: undefined
    },
    // 同 el-upload 的 before-upload
    beforeUpload: {
      type: Function,
      default: undefined
    },
    // 同 el-upload 的 on-preview
    onPreview: {
      type: Function,
      default: undefined
    },
    // 同 el-upload 的 http-request
    httpRequest: {
      type: Function,
      default: undefined
    },
    // 同时上传的并发数量
    concurrentNumber: {
      type: Number,
      default: 50
    }
  },
  data() {
    return {
      refId: `upload-${Math.random()}-${Date.now()}`,
      // 总上传的文件夹列表，包含回显的、多次上传之后的
      directories: [],
      // 旧的文件列表
      oldFileList: [],
      // 上传前的文件列表，只有当前选中的文件，用于并发上传控制
      beforeUploadList: [],
      // 记录失败的文件列表
      beforeUploadFailList: []
    }
  },
  watch: {
    // 文件夹状态更新后实时切换过去
    directory() {
      this.onDirectoryChange()
    },
    value(newValue) {
      // 如果是文件夹上传但是执行了删除或者置空 value 的时候，同步清空 directories，否则会出现再次选择文件时累加的情况
      if (this.showDirectories && this.directories.length && newValue.length <= 0) {
        this.directories = []
        // 因为没有同步 fileList 字段进去，删除的时候同步清空组件内部的文件列表，否则再次上传就会出现上一次的文件
        this.$refs[this.refId].uploadFiles = []
      }
      // 初始化的时候传递了默认值的话同步到 directories 里面
      if (this.showDirectories && !this.directories.length && newValue.length > 0) {
        this.directories = [...this.fileList]
        this.oldFileList = JSON.parse(JSON.stringify(this.directories))
      }
    }
  },
  computed: {
    // value 字段兼容
    fileList() {
      if (this.showDirectories) {
        return this.value
          .map(item => {
            return {
              ...item,
              files: this.transformFields(item.files || [])
            }
          })
      }
      return this.transformFields(this.value)
    },
    // 事件合并
    customListeners() {
      return {
        ...this.$listeners
      }
    },
    // 属性合并
    customAttrs() {
      const attrs = { ...this.$attrs, action: this.actionUrl }
      // 如果是上传文件夹且没有定义过自定义上传，传递内置的自定义上传方法
      if (this.directory && !this.httpRequest) {
        attrs.httpRequest = this.onAddUploadList
      } else if (this.httpRequest) {
        attrs.httpRequest = this.httpRequest
      }
      // 如果是要显示成文件夹进度
      if (this.showDirectories) {
        attrs.multiple = true
        attrs.showFileList = false
      } else {
        // 如果是文件夹上传且要显示文件列表的话，强制设置显示列表
        if (this.directory) {
          attrs.multiple = true
          attrs.showFileList = true
        }
        attrs.fileList = this.fileList
      }
      return attrs
    },
    actionUrl() {
      return this.action || this.$ex_default_upload_url || '#'
    },
    realDownloadUrl() {
      return this.downloadUrl || this.$ex_default_download_url
    },
    // 是否显示文件夹列表
    showDirectories() {
      return this.directory && this.showDirectory
    }
  },
  created() {
    if (!this.supportDirectory()) {
      this.$message.error('当前浏览器不支持文件夹上传')
      return
    }
    // 初始化的时候传递了默认值的话同步到 directories 里面
    if (this.showDirectories && !this.directories.length && this.value.length > 0) {
      this.directories = [...this.fileList]
      this.oldFileList = JSON.parse(JSON.stringify(this.directories))
    }
  },
  mounted() {
    this.onDirectoryChange()
  },
  methods: {
    // 字段转换
    transformFields(items) {
      return items.map(item => {
        const name = item.name || item.fileName || ''
        const url = item.url || item.filePath || ''
        if (url) {
          return {
            // 内置设置 name 和 url 字段，因为数据回显的时候，返回的可能只有 fileName 和 filePath
            name,
            url,
            fileName: name,
            filePath: url,
            uid: `${name}-${Date.now().toString(36)}-${Math.random().toString(36)}`,
            status: 'success',
            ...item
          }
        }
        return { ...item }
      })
    },
    // 返回处理后的数据
    getUploadData() {
      // 如果是文件夹上传，将外层包裹的文件夹也返回去，因为回显也需要
      if (this.showDirectories) {
        return this.fileList.slice()
          .map(item => {
            // 将旧的文件列表合并过来
            const oldFile = this.oldFileList.find(file => file.uid === item.uid) || {}
            const files = [...item.files]
            // 如果存在一开始就传递进来的文件
            if (oldFile.files && oldFile.files.length) {
              oldFile.files.forEach(oldFile => {
                // 只有最新的列表里面没有才合并
                if (!files.find(file => file.uid === oldFile.uid)) {
                  files.push(oldFile)
                }
              })
            }
            if (files.length) {
              return {
                ...item,
                files: files
                  // 只返回上传成功的数据
                  .filter(item => item.status === 'success')
                  .map(file => {
                    return {
                      // 全路径
                      relativePath: file.relativePath || (file.raw ? file.raw.webkitRelativePath : ''),
                      // 文件名称
                      fileName: file.fileName || '',
                      filePath: file.filePath || ''
                    }
                  })
              }
            }
            return { ...item }
          })
      }
      return this.fileList.slice()
        // 只返回上传成功的数据
        .filter(item => item.status === 'success')
        .map(item => {
          return {
            fileName: item.fileName || '',
            filePath: item.filePath || ''
          }
        })
    },
    /**
     *
     * 树结构形式的文件结构
     *
     * const data = [{
        dirname: '文件夹1',
        dirs: [{
          dirname: '文件夹2',
          dirs: [{
            dirname: '文件夹3',
            dirs: [],
            files: [{
              fileName: '手机桌面背景3.jpeg',
              filePath: 'default/20210121/b06cf313-8ae6-4d7f-9d63-40555a2c1bf0.jpeg'
            }]
          }],
          files: [{
            fileName: '手机桌面背景3.jpeg',
            filePath: 'default/20210121/b06cf313-8ae6-4d7f-9d63-40555a2c1bf0.jpeg'
          }]
        }],
        files: [{
          fileName: '手机桌面背景3.jpeg',
          filePath: 'default/20210121/b06cf313-8ae6-4d7f-9d63-40555a2c1bf0.jpeg'
        }]
      }]
     */
    // todo：目前没有这个场景，未来有了再开放入口
    treeDirs(files) {
      const data = []
      files.forEach((item, i) => {
        const dirs = {
          dirname: item.dirname.replace('/', ''),
          dirs: [],
          files: []
        }
        // 如果是第一次先添加进去
        if (!data[i]) {
          data[i] = dirs
        }
        if (item.files && item.files.length) {
          item.files.forEach(file => {
            const names = file.relativePath.split('/').slice(1, -1)
            if (names.length) {
              this.fileNameToDir(file, names, dirs.dirs)
            } else { // 否则说明当前文件是属于顶层目录的
              dirs.files.push({
                fileName: file.fileName,
                filePath: file.filePath
              })
            }
          })
        }
      })
      return data
    },
    // 根据文件名创建目录
    fileNameToDir(file, names, dirs) {
      const dirname = names.shift()
      const currentDirs = dirs.find(item => item.dirname === dirname)
      // 如果当前文件夹已存在
      if (currentDirs) {
        if (!names.length) {
          currentDirs.files.push({
            fileName: file.fileName,
            filePath: file.filePath
          })
        } else {
          this.fileNameToDir(file, names, currentDirs.dirs)
        }
      } else { // 不然就新建一个文件夹
        const childDirs = {
          dirname,
          dirs: [],
          files: []
        }
        dirs.push(childDirs)
        // 如果没有 name 了说明文件也找到了归属
        if (!names.length) {
          childDirs.files.push({
            fileName: file.fileName,
            filePath: file.filePath
          })
        } else {
          this.fileNameToDir(file, names, childDirs.dirs)
        }
      }
    },
    // 上传之前过滤掉隐藏文件
    onBeforeUpload(file) {
      // 如果外部传递了回调就用
      let result = true
      if (this.beforeUpload) {
        result = this.beforeUpload(file)
      }
      // 返回了 false 就停止下面的操作，否则继续
      if (!result) {
        return false
      }
      // 过滤掉隐藏文件
      if (file.name.match(/^\./)) {
        return false
      }
      // 如果是上传文件夹
      if (this.showDirectories) {
        const dirname = `${file.webkitRelativePath.split('/').shift()}/`
        // 如果当前没有找到文件夹就新增一个
        const currentDirname = this.directories.find(dir => dir.dirname === dirname)
        if (!currentDirname) {
          this.directories.push({
            // 生成随机字符串
            uid: `${dirname}-${Date.now().toString(36)}-${Math.random().toString(36)}`,
            dirname,
            current: 0,
            total: 1,
            files: [file]
          })
        } else { // 否则增加总计
          currentDirname.total += 1
          currentDirname.files.push(file)
        }
      }
      return true
    },
    // 受控文件
    onUploadChange(file, fileList) {
      // 如果是文件夹上传且要返回文件夹列表
      if (this.showDirectories) {
        // ! 等待上一次的文件变动更新完了之后再更新，否则会出现数据丢失的情况
        this.$nextTick(() => {
          const values = this.directories
          const res = values.map(dir => {
            const files = fileList.filter(item => _.startsWith(item.raw.webkitRelativePath, dir.dirname))
            dir.files = files
            return dir
          })
          this.$emit('input', res)
          if (this.onChange) {
            this.onChange(file, fileList)
          }
        })
      } else {
        this.$nextTick(() => {
          this.$emit('input', fileList)
          if (this.onChange) {
            this.onChange(file, fileList)
          }
        })
      }
    },
    // 上传成功
    onUploadSuccess(res, file, fileList) {
      // 预防万一还是判断一下
      if (res && res.code === 0) {
        const { fileName, filePath } = res.data || {}
        const currentFile = fileList.find(item => item.uid === file.uid) || {}
        currentFile.fileName = fileName || currentFile.name
        currentFile.filePath = filePath || ''
      }
      // 更新上传进度
      if (this.showDirectories) {
        const values = this.directories
        const currentDir = values.find(item => _.startsWith(file.raw.webkitRelativePath, item.dirname))
        if (currentDir) {
          currentDir.current += 1
        }
      }
      if (this.onSuccess) {
        this.onSuccess(res, file, fileList)
      }
    },
    // 文件删除回调
    onUploadRemove(file, fileList) {
      // 如果是文件夹上传，还要删除内部缓存的数据
      if (this.showDirectories) {
        // 因为没有同步 fileList 字段进去，删除的时候同步清空组件内部的文件列表，否则再次上传就会出现上一次的文件
        if (file && file.files && file.files.length) {
          file.files.forEach(item => {
            const index = this.$refs[this.refId].uploadFiles.findIndex(f => f.uid === item.uid)
            if (index !== -1) {
              this.$refs[this.refId].uploadFiles.splice(index, 1)
            }
          })
        }
        const index = this.directories.findIndex(item => item.uid === file.uid)
        if (index !== -1) {
          this.directories.splice(index, 1)
        }
      }
      this.$nextTick(() => {
        this.$emit('input', fileList.filter(item => item.uid !== file.uid))
        // 删除的回调
        if (this.onRemove) {
          this.onRemove(file, fileList)
        }
      })
    },
    // 上传的文件超出个数限制
    onUploadExceed(file, fileList) {
      this.$message.error(`最多支持上传 ${this.customAttrs.limit} 个文件`)
      if (this.onExceed) {
        this.onExceed(file, fileList)
      }
    },
    // 预览文件
    onFilePreview(file) {
      let url = file.url || file.filePath
      if (!url) {
        this.$message.error('没有获取到图片路径，预览失败')
        return
      }
      if (!url.match(/^(http|blob:http)/)) {
        if (!this.realDownloadUrl) {
          this.$message.error('没有配置文件预览路径')
          return
        }
        url = this.realDownloadUrl + url
      }
      window.open(url, '_blank')
      if (this.onPreview) {
        this.onPreview(file)
      }
    },
    // 监听上传变化，实时调用 input
    // 文件夹上传切换
    onDirectoryChange() {
      const form = this.$refs[this.refId].$el.querySelector('input[type="file"]')
      if (form) {
        if (this.directory) {
          form.setAttribute('webkitdirectory', 'webkitdirectory')
        } else {
          form.removeAttribute('webkitdirectory')
        }
      }
    },
    // 兼容判断
    supportDirectory() {
      const input = document.createElement('input')
      input.type = 'file'
      return 'webkitdirectory' in input || 'directory' in input
    },
    // 上传前收集文件列表
    onAddUploadList(file) {
      this.beforeUploadList.push(file)
      clearTimeout(this.timer)
      this.timer = null
      // 文件夹上传的时候，是所有文件处理就绪之后再来调用的本方法，所以只要调用了这个方法就说明文件列表已经是全部的了，然后再启动并发上传
      this.timer = setTimeout(() => {
        clearTimeout(this.timer)
        this.timer = null
        this.runConcurrentUpload()
          .then(res => {
            console.log('上传结果')
            console.log(res)
            const length = this.beforeUploadFailList.length
            this.$message.success(`文件夹上传成功${length ? `，本次上传 ${length} 个文件上传失败` : ''}`)
            // 清空上一次失败的文件
            this.beforeUploadFailList = []
          })
      }, 300)
    },
    // 启动并发上传
    runConcurrentUpload() {
      const recursion = (arr) => {
        const currentFile = arr.shift()
        return this.onCustomUpload(currentFile)
          .then(() => {
            // 如果还没上传完，继续
            if (arr.length !== 0) {
              return recursion(arr)
            }
            return true
          })
          .catch(() => {
            // 如果失败了进行一次重试
            return this.onCustomUpload(currentFile)
              .then(() => {
                // 如果还没上传完，继续
                if (arr.length !== 0) {
                  return recursion(arr)
                }
                return true
              })
              .catch(res => {
                // 记录失败的文件
                this.beforeUploadFailList.push(currentFile)
                // 再次尝试也失败了就直接抛出错误
                const tips = res.message || res.msg
                this.$message.error(`文件 ${currentFile.file.name} 进行第一次重新上传失败${tips ? `，原因：${tips}` : ''}`, 3000)
                currentFile.onError(res)
                // 如果还有文件没上传，再次开启上传
                if (arr.length !== 0) {
                  return recursion(arr)
                }
                return false
              })
          })
      }
      // 并发数
      // 如果文件总数不足最大并发数，取当前文件总数
      let limit = Math.min(this.concurrentNumber, this.beforeUploadList.length)
      // 正在进行的所有并发异步操作
      const asyncList = []
      while (limit--) {
        asyncList.push(recursion(this.beforeUploadList))
      }
      // 所有并发异步操作都完成后，本次并发控制迭代完成
      return Promise.all(asyncList)
    },
    // 如果是文件夹上传
    onCustomUpload(file) {
      const formData = new FormData()
      formData.append('file', file.file)
      formData.append('directory', file.file.webkitRelativePath.split('/').slice(0, -1).join('/'))

      return axios.post(this.actionUrl, formData)
        .then(res => res.data)
        .then(res => {
          // 成功之后直接调用成功的回调
          if (res.code === 0) {
            file.onSuccess(res)
            return res
          } else { // code 不是 0 的处理成失败
            return Promise.reject(res)
          }
        })
    }
  }
}
</script>

<style lang="less" scoped>
.ex-upload-wrap {
  .directory-list-main {
    position: relative;
    padding-top: 10px;

    .progress-item {
      padding: 4px;
      border-radius: 2px;

      &:hover {
        background-color: #F5F7FA;

        .name {
          color: #409EFF;
        }

        .el-icon-close {
          opacity: 1;
          visibility: visible;
        }
      }

      /deep/ .el-progress-bar__inner {
        transition: width 0.2s ease;
      }
    }

    .name-box {
      display: flex;
      justify-content: space-between;
      align-items: center;
      transition: all 0.25s;

      .name {
        font-size: 12px;
      }

      .el-icon-close {
        font-size: 14px;
        cursor: pointer;
        transition: all 0.25s;
        opacity: 0;
        visibility: hidden;
      }
    }
  }

  /deep/ .el-progress__text {
    font-size: 12px !important;
  }
}
</style>
